﻿using System;
using System.Collections.Generic;
using System.Text;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using Microsoft.Xna.Framework.Graphics;

using Form = System.Windows.Forms.Form;

namespace Cobalt.Content.Pipeline.Graphics.Illumination
{
	partial class IlluminationEngine
	{

		private Form deviceWnd;
		
		private GraphicsDevice device;

		private VertexDeclaration lightDecl;

		private int resolution;
		private Vector4[] fetchBuffer;
		private RenderTarget2D[] accumTargets = new RenderTarget2D[1024];
		private DepthStencilBuffer depthTarget;

		private EffectPool effectPool;
		private Effect lightEffect;

		private Texture2D weightMask;
		private SpriteBatch spriteBatch;

		private void InitializeGraphics()
		{
			if( device != null )
				return;

			//create the device

			GraphicsAdapter adapter = GetBestAdapter();

			deviceWnd = new Form();

			PresentationParameters presParams = new PresentationParameters();
			presParams.EnableAutoDepthStencil = false;
			presParams.BackBufferWidth = 1;
			presParams.BackBufferHeight = 1;
			presParams.BackBufferCount = 1;
			presParams.DeviceWindowHandle = deviceWnd.Handle;

			device = new GraphicsDevice( adapter, DeviceType.Hardware,
				deviceWnd.Handle, presParams );

			//set up the render resolution

			int maxRes = Math.Min( device.GraphicsDeviceCapabilities.MaxTextureWidth,
				device.GraphicsDeviceCapabilities.MaxTextureHeight );
			resolution = Math.Min( sampleResolution, maxRes );

			//various objects
			lightDecl = new VertexDeclaration( device, new VertexElement[]
			{
				new VertexElement( 0, 0, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Position, 0 ),
			} );

			weightMask = new Texture2D( device, resolution, resolution, 1,
				TextureUsage.None, SurfaceFormat.Vector4 );
			weightMask.SetData( GenerateWeightMask() );

			spriteBatch = new SpriteBatch( device );

			//create the render target(s)

			fetchBuffer = new Vector4[resolution * resolution * 5];

			for( int i = 0; i < accumTargets.Length; i++ )
			{
				accumTargets[i] = new RenderTarget2D( device, resolution, resolution * 5,
					1, SurfaceFormat.Vector4, RenderTargetUsage.PlatformContents );
			}

			depthTarget = new DepthStencilBuffer( device, resolution, resolution * 5, DepthFormat.Depth24 );

			//load up the effects
			effectPool = new EffectPool();
			lightEffect = CompileEffect( Properties.Resources.LightingLight_fx );
		}

		private Vector4[] GenerateWeightMask()
		{
			Vector4[] ret = new Vector4[resolution * resolution];

			float halfPixWidth = (float)(projZNear * Math.Tan( 0.5 * projFov ));
			float pixWidth = 2 * halfPixWidth;

			float offset = -pixWidth * resolution / 2;

			float projZNear2 = projZNear * projZNear;

			int io = 0;
			for( int y = 0; y < resolution; y++ )
			{
				float py = offset + pixWidth * y + halfPixWidth;

				for( int x = 0; x < resolution; x++ )
				{
					float px = offset + pixWidth * x + halfPixWidth;

					double d2 = px * px + py * py;
					double d = Math.Sqrt( d2 );

					double f = Math.Atan( (d + pixWidth) / projZNear ) - Math.Atan( d / projZNear );
					double c = projZNear / Math.Sqrt( d2 + projZNear2 );

					float v = (float)(5 * f * c);
					ret[io++] = new Vector4( v, v, v, 1 );
				}
			}

			float sum = 0;
			for( int i = 0; i < ret.Length; i++ )
				sum += ret[i].X;

			//System.Diagnostics.Debugger.Launch();

			/*
			float scale = 1.0F / sum;
			for( int i = 0; i < ret.Length; i++ )
			{
				ret[i].X *= scale;
				ret[i].Y *= scale;
				ret[i].Z *= scale;
			}
			*/

			return ret;
		}

		private Effect CompileEffect( string code )
		{
			CompiledEffect compiled = Effect.CompileEffectFromSource( code, null, null,
				CompilerOptions.None, TargetPlatform.Windows );

			if( !compiled.Success )
				throw new Exception( "Error compiling lightmapper effect code.\n" + compiled.ErrorsAndWarnings );

			return new Effect( device, compiled.GetEffectCode(), CompilerOptions.NotCloneable, effectPool );
		}

		private void DisposeGraphics()
		{
			DisposeSurfaceGraphics();
			for( int i = 0; i < accumTargets.Length; i++ )
				Helpers.DisposeAndNull( ref accumTargets[i] );
			Helpers.DisposeAndNull( ref device );
			Helpers.DisposeAndNull( ref deviceWnd );
		}

		private GraphicsAdapter GetBestAdapter()
		{
			return GraphicsAdapter.DefaultAdapter;
		}

		#region Surface
		protected internal sealed class Surface : IDisposable
		{
			public GeometryContent Geometry;
			public BoundingBox Bounds;

			public Surface( GeometryContent geom )
			{
				this.Geometry = geom;
				Bounds = BoundingBox.CreateFromPoints( geom.Vertices.Positions );
			}

			private List<LightContent> lights = new List<LightContent>();
			public List<LightContent> LinkedLights { get { return lights; } }

			private GraphicsDevice device;
			private VertexDeclaration decl;
			private int declStride;
			private VertexBuffer vb;
			private IndexBuffer ib;

			public bool HasLoadedGraphics { get { return device != null; } }

			public void InitGraphicsObjects( GraphicsDevice device )
			{
				//recreate if exists, in case mesh has changed
				DisposeGraphicsObjects();

				VertexBufferContent vbContent;
				VertexElement []vbElems;

				this.device = device;

				Geometry.Vertices.CreateVertexBuffer( out vbContent, out vbElems, TargetPlatform.Windows );

				decl = new VertexDeclaration( device, vbElems );
				declStride = decl.GetVertexStrideSize( 0 );
				
				vb = new VertexBuffer( device, vbContent.VertexData.Length, BufferUsage.None );
				vb.SetData( vbContent.VertexData );

				uint[] indices = new uint[Geometry.Indices.Count];
				for( int i = 0; i < indices.Length; i++ )
					indices[i] = (uint)Geometry.Indices[i];

				ib = new IndexBuffer( device, typeof( uint ), indices.Length, BufferUsage.None );
				ib.SetData( indices );
			}

			public void DisposeGraphicsObjects()
			{
				this.device = null;
				Helpers.DisposeAndNull( ref decl );
				Helpers.DisposeAndNull( ref vb );
				Helpers.DisposeAndNull( ref ib );
			}

			public void BeginRender()
			{
				device.VertexDeclaration = decl;
				device.Vertices[0].SetSource( vb, 0, declStride );
				device.Indices = ib;
			}

			public void Render()
			{
				device.DrawIndexedPrimitives( PrimitiveType.TriangleList, 0, 0,
					Geometry.Vertices.Positions.Count, 0, Geometry.Indices.Count / 3 );
			}

			public void EndRender()
			{
			}

			#region IDisposable Members

			public void Dispose()
			{
				DisposeGraphicsObjects();
			}

			#endregion
		}

		private void InitializeSurfaceGraphics()
		{
			foreach( Surface surf in surfaces )
				surf.InitGraphicsObjects( device );
		}

		private void DisposeSurfaceGraphics()
		{
			foreach( Surface surf in surfaces )
				surf.Dispose();
		}
		#endregion

		private struct FetchJob
		{
			public RenderTarget2D RenderTarget;
			public SampleTarget LightmapTarget;
		}

		private Queue<FetchJob> jobs = new Queue<FetchJob>();

		private RenderTarget2D GetTarget( SampleTarget sampleTarget )
		{
			FetchJob job;

			//prime the queue
			if( jobs.Count < accumTargets.Length )
			{
				job = new FetchJob();
				job.RenderTarget = accumTargets[jobs.Count];
			}
			else
			{
				job = jobs.Dequeue();
				FinishJob( job );
			}

			job.LightmapTarget = sampleTarget;
			jobs.Enqueue( job );

			return job.RenderTarget;
		}

		private void FinishRender()
		{
			while( jobs.Count != 0 )
				FinishJob( jobs.Dequeue() );
		}

		private unsafe void FinishJob( FetchJob job )
		{
			job.RenderTarget.GetTexture().GetData( fetchBuffer );

			Vector3 value = new Vector3();

			fixed( Vector4 *buf = fetchBuffer )
			{
				for( int y = 0; y < resolution; y++ )
				{
					Vector4 *row = buf + y * resolution * 5;
					int span = y < (resolution / 2) ? resolution * 5 : resolution;

					for( int x = 0; x < span; x++ )
					{
						value.X += row[x].X;
						value.Y += row[x].Y;
						value.Z += row[x].Z;
					}
				}
			}

			job.LightmapTarget.Target.SetLuxel( DefaultLightmapLayer,
				job.LightmapTarget.SampleIndex, ref value );
		}

		private void LightSample_FullIllum( ref Lightmap.SampleDesc desc, SampleTarget sampleTarget )
		{
			if( desc.Source.LinkedLights.Count == 0 )
				return;

			RenderTarget2D target = GetTarget( sampleTarget );

			device.SetRenderTarget( 0, target );
			device.DepthStencilBuffer = depthTarget;

			device.Clear( ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0F, 0 );

			device.RenderState.CullMode = CullMode.None;
			device.RenderState.ColorWriteChannels = ColorWriteChannels.All;
			device.RenderState.AlphaBlendEnable = true;
			device.RenderState.SourceBlend = Blend.One;
			device.RenderState.DestinationBlend = Blend.One;
			device.RenderState.BlendFunction = BlendFunction.Add;
			device.RenderState.AlphaTestEnable = false;
			device.RenderState.DepthBufferEnable = true;
			device.RenderState.DepthBufferFunction = CompareFunction.LessEqual;
			device.RenderState.DepthBufferWriteEnable = true;

			lightEffect.Parameters["SamplePos"].SetValue( desc.Position );
			lightEffect.Parameters["SampleNorm"].SetValue( desc.Normal );
			lightEffect.Parameters["SampleTan"].SetValue( desc.Tangent );
			lightEffect.Parameters["SampleBin"].SetValue( desc.Binormal );

			DrawLightView( ref desc, desc.Normal, desc.Tangent, 0 );
			DrawLightView( ref desc, desc.Tangent, desc.Normal, 1 );
			DrawLightView( ref desc, -desc.Tangent, desc.Normal, 2 );
			DrawLightView( ref desc, desc.Binormal, desc.Normal, 3 );
			DrawLightView( ref desc, -desc.Binormal, desc.Normal, 4 );

			device.SetRenderTarget( 0, null );
		}

		private static readonly float projFov = MathHelper.ToRadians( 90 );
		private const float projZNear = 0.1F;
		private const float projZFar = 1000.0F;
		private static readonly Matrix projMatrix = Matrix.CreatePerspectiveFieldOfView( projFov, 1, projZNear, projZFar );

		private void DrawLightView( ref Lightmap.SampleDesc desc, Vector3 forward, Vector3 up, int window )
		{
			device.Viewport = new Viewport()
			{
				X = resolution * window,
				Y = 0,
				Width = resolution,
				Height = resolution,
				MinDepth = 0.0F,
				MaxDepth = 1.0F,
			};

			Matrix view = Matrix.CreateLookAt( desc.Position, desc.Position + forward, up );

			lightEffect.Parameters["MatView"].SetValue( view );
			lightEffect.Parameters["MatViewProj"].SetValue( view * projMatrix );

			Vector3[] posBuf = new Vector3[1];
			foreach( LightContent light in desc.Source.LinkedLights )
			{
				float pointSize = 1;

				lightEffect.CurrentTechnique = lightEffect.Techniques["PointLight"];
				lightEffect.Parameters["LightValue"].SetValue( light.LightColor *
					(resolution * resolution) / (pointSize * pointSize) );
				lightEffect.Parameters["LightAtten"].SetValue( light.AttenParams );
				lightEffect.Parameters["LightPointSize"].SetValue( pointSize );

				posBuf[0] = light.Position;
				device.VertexDeclaration = lightDecl;

				lightEffect.Begin();
				foreach( EffectPass pass in lightEffect.CurrentTechnique.Passes )
				{
					pass.Begin();

					device.DrawUserPrimitives( PrimitiveType.PointList, posBuf, 0, 1 );

					pass.End();
				}
				lightEffect.End();
			}

			spriteBatch.Begin( SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.None );
			device.RenderState.AlphaBlendEnable = true;
			device.RenderState.AlphaBlendOperation = BlendFunction.Add;
			device.RenderState.SourceBlend = Blend.DestinationColor;
			device.RenderState.DestinationBlend = Blend.Zero;

			spriteBatch.Draw( weightMask, new Rectangle( 0, 0, resolution, resolution ), Color.White );

			spriteBatch.End();
		}

	}
}
